/*
 * Security.java
 *
 * Created on Dec 9, 2009 9:45:03 AM
 */

package encryption;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
import java.util.UUID;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;

import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * Methods for hashing, encryption and decrypting passwords
 * @author Marc Coronado
 */
public class Security {

    public static enum EncryptionType{
        MD5("MD5"), SHA256("SHA256");

        private final String algorithmName;
        EncryptionType(String algorithmName) {
            this.algorithmName = algorithmName;
        }
        public String algorithmName() {
            return algorithmName;
        }
    }

    // Magic constants of the universe
    private static byte[] GLOBAL_ENCRYPTION_PASSWORD;  // set in init() below
    private static byte[] iv = new byte[]{
        (byte)-8,  (byte)-7  , (byte)-6, (byte)-5, (byte)-4 , (byte)-3 , (byte)-2 , (byte)-1,
        (byte)1, (byte)2  , (byte)3, (byte)4, (byte)5, (byte)6  , (byte)7, (byte)8
    };
    
    // Boot Config: Strength of all hashing and encryption
    public static EncryptionType ENCRYPTION_STRENGTH;

    /** @return the strength of all encryrption and hashing */
    public static EncryptionType getEncryptionStrength() {
        return ENCRYPTION_STRENGTH;
    }
   
    // Must be set after ENCRYPTION_STRENGTH
    public static void init() {
        GLOBAL_ENCRYPTION_PASSWORD = hashPasswordBytes("SECURITY_KEY");
    }

    /** Main method for encryption operations
     * @param args
     * @see #mainUsage()
     */
    public static void main(String[] args) {
        if (args.length==0)
            mainUsage();
        String command = args[0];
        if (command.equals("encrypt") || command.equals("decrypt")) {
            if (args.length<2 || args.length>4)
                mainUsage();
            EncryptionType strength = EncryptionType.MD5;
            String         customer = null;
            String         password = args[1];
            if (args.length>2) {
                customer = args[2];
                if (customer.equals("null"))
                    customer = null;
                if (args.length>3)
                    try {
                        strength = EncryptionType.valueOf(args[3]);
                    } catch (IllegalArgumentException e) {
                        mainUsage();
                    }
            }
            ENCRYPTION_STRENGTH = strength;
            init();
            System.out.println(command.equals("encrypt") ? encrypt(password, customer) : decrypt(password, customer));
        } else
            mainUsage();
    }

    /** Show usage for main() and exit with error */
    public static void mainUsage() {
        System.out.println("Usage:  java -jar Encryption.jar [encrypt|decrypt] [text] <salt> <MD5|SHA256>");
        System.exit(1);
    }

    // ******** Hashing and encryption

    /** Hash a password using configured strength
     * @param password the password
     * @return the base64 representation of the hash of password
     */
    public static String hashPassword(String password) {
        return base64Encode(hashPasswordBytes(password));
    }

    /** Hash a password using configured strength
     * @param password the password
     * @return the hash of password
     */
    public static byte[] hashPasswordBytes(String password) {
        return hashPasswordBytes(password, ENCRYPTION_STRENGTH);
    }

    private static byte[] hashPasswordBytes(String src, EncryptionType method) {
        if(src == null)
            src = "";
        String salt = "default_salt";
        try{
            MessageDigest md  = MessageDigest.getInstance(method.algorithmName());
            md.update(src .getBytes("UTF-8"));
            md.update(salt.getBytes("UTF-8"));
            return md.digest();
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);     //should be impossible
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);     //should be impossible
        }
    }

    /** Encrypt a string using a fixed internal key
     * @param src the string
     * @param salt if non-null, salt (allows to be customer-specific if customer name or fqdn used)
     * @return the encryption of string
     * @see #decrypt(java.lang.String)
     */
    public static String encrypt(String src, String salt) {
        try {
            Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, GLOBAL_ENCRYPTION_PASSWORD, salt);
            return base64Encode(cipher.doFinal(src.getBytes("UTF-8")));
        } catch (Exception e) {
            throw new RuntimeException(e);    // should be impossible
        }
    }

    /** Decrypt a string using a fixed internal key
     * @param src the string
     * @param salt salt used in encrypt()
     * @return the decryption of string
     * @see #encrypt(java.lang.String)
     */
    public static String decrypt(String src, String salt) {
        try {
            Cipher cipher = getCipher(Cipher.DECRYPT_MODE, GLOBAL_ENCRYPTION_PASSWORD, salt);
            return new String(cipher.doFinal(base64Decode(src)), "UTF-8");
        } catch (Exception e) {
            throw new RuntimeException(e);    // should be impossible
        }
    }

    /** Get an encryption cipher useful for operating on values also operated on by client-side components (e.g., passwords
     *  stored by client-side components)
     * @param mode either Cipher.ENCRYPT_MODE or Cipher.DECRYPT_MODE
     * @param password the password used to generate the key
     * @param salt salt added to the password (e.g., customer name); may be null to none
     * @return the cipher, all initialized and ready to be used
     */
    public static Cipher getCipher(int mode, byte[] password, String salt) {
        return getCipher(mode, password, salt, iv);
    }

    /** Get an encryption cipher useful only for values that only the server operates on
     * @param mode either Cipher.ENCRYPT_MODE or Cipher.DECRYPT_MODE
     * @param password the password used to generate the key
     * @param salt salt added to the password (e.g., customer name); may be null to none
     * @return the cipher, all initialized and ready to be used
     * @see #getCipher(int, byte[], java.lang.String)
     */
    public static Cipher getInternalCipher(int mode, byte[] password, String salt) {
        return getCipher(mode, password, salt, iv);
    }

    // Work method for getCipher() and getInternalCipher()
    private static Cipher getCipher(int mode, byte[] password, String salt, byte[] iv) {
        try {
            // First create the key by hashing the password and salt
            MessageDigest md = MessageDigest.getInstance(ENCRYPTION_STRENGTH.algorithmName());
            md.update(password);
            if (salt!=null) {
                md.update(salt.getBytes("UTF-8"));
            }
            byte[] rawkey = md.digest();
            byte[] key = new byte[ENCRYPTION_STRENGTH==EncryptionType.SHA256 ? 32 : 16];
            System.arraycopy(rawkey, 0, key, 0, Math.min(rawkey.length, key.length));

            // Then create the cipher using the key as primary encryptor, and fixed parms for consistent use elsewhere
            Key spec = new SecretKeySpec(key, "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/ISO10126Padding");
            AlgorithmParameterSpec parms = new IvParameterSpec(iv);
            cipher.init(mode, spec, parms);
            return cipher;
        } catch (Exception e) {
            throw new RuntimeException(e);    // should be impossible
        }
    }

    /**
     * Base64 encodes the input byte[]
     * @param src input byte[]
     * @return base64 encoded string
     */
    public static String base64Encode(byte[] src) {
        return src == null ? "" : new String(Base64.encodeToString(src));
    }

    /**
     * Base64 decodes the input string
     * @param encoded base64-encoded string
     * @return bytes encoded by encoded
     */
    public static byte[] base64Decode(String encoded) {
        try {
            return encoded == null ? new byte[]{} : Base64.decode(encoded.getBytes("US-ASCII"));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);      // impossible
        }
    }
   
    /** Returns a decrypted inputstream, using an <em>internal<.em> cipher
     * @param srcStream the inputstream to decrypt
     * @param salt if non-null, salt (allows to be customer-specific if customer name or fqdn used)
     * @param hashedPasswordBytes : Hashedpassword to be used to decrypt the passed stream
     * @return CipherInputStream encrypted stream that needs to be decrypted
     * @see #getEncryptedOutputStream(OutputStream, String, byte[])
     * @see #getInternalCipher(int, byte[], java.lang.String)
     */
    public static CipherInputStream getInternalDecryptedInputStream(InputStream srcStream, String salt,
                                                                    byte[] hashedPasswordBytes) {
        try {
            Cipher cipher = getInternalCipher(Cipher.DECRYPT_MODE, hashedPasswordBytes, salt);
            return new CipherInputStream (srcStream, cipher);
        } catch (Exception e) {
            throw new RuntimeException(e);    // should be impossible
        }
    }

    /** Returns a encrypted outputstream, using an <em>internal<.em> cipher
     * @param srcStream the output stream to encrypt
     * @param salt if non-null, salt (allows to be customer-specific if customer name or fqdn used)
     * @param hashedPasswordBytes : Hashedpassword to be used to ecrypt the passed stream
     * @return CipherOutputStream dencrypted stream that needs to be encrypted
     * @see #getDecryptedInputStream(OutputStream, String, byte[])
     * @see #getInternalCipher(int, byte[], java.lang.String)
     */
    public static CipherOutputStream getInternalEncryptedOutputStream(OutputStream srcStream, String salt,
                                                                      byte[] hashedPasswordBytes) {
        try {
            Cipher cipher = getInternalCipher(Cipher.ENCRYPT_MODE, hashedPasswordBytes, salt);
            return new CipherOutputStream(srcStream , cipher);
        } catch (Exception e) {
            throw new RuntimeException(e);    // should be impossible
        }
    }

    public static String bytesToHex(byte[] data){
        StringBuilder hex = new StringBuilder();
        for (int i=0; i < data.length; i++) {
            hex.append(Integer.toString( ( data[i] & 0xff ) + 0x100, 16).substring(1));
        }

        return hex.toString().toUpperCase();
    }

    /**
     * Create a UUID
     * @return String version formated without dashes and upercase 32 chars long
     */
    public static String createUUID(){
        UUID u = UUID.randomUUID();
        return u.toString().replace("-", "").toUpperCase();
    }
}
